#! /usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2011 BidiX (http://bidix.info)
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License. entities
#
"""
	This module implements the Data model. Notewiki	App uses GAE Datastore for persistence of content. 

	
	The main class of the model is the :class:`notewiki.model.Article` class. 
	:class:`notewiki.model.Article` manage the content of the Notewiki and its organization.
	
	The :class:`notewiki.model.Entity` is the super class of notewiki model classes.
	
	
"""

import sys, datetime, time, logging, re
from google.appengine.ext import db
from google.appengine.api import users
from notewiki.ui import render_str, render_user
import xml.dom.minidom
import webapp2


def area_for(name):
	"""
	The accessibility of this article.
	
	Arg:
		name (string): a name of an article
		 
	Returns:
		:Class:`notewiki.model.Area`: the :Class:`notewiki.model.Area` for this article name.

	See:
		`link notewiki/doc/definitions/area <http://notewiki.bidix.info/notewiki/doc/definitions/area>`_.
	"""
	f = root_folder(name)
	if f == '_':
		return Area.shared
	if f == '__':
		return Area.sandbox
	if f and f[0] == '_':
		return Area.private
	return Area.public

#@deprecated
#def convert_unicode_keywords(kwds):
#	"""
#	Convert unicode keywords in string kwords.
#	
#	See bug : Python does not accept unicode keywords <http://bugs.python.org/issue2646>
#	
#	Corrected in Python 2.6. Suppress this function in python 2.7
#	"""
#	new_kwds = {}
#	for (key, value) in kwds.items():
#		new_kwds[str(key)] = value
#	return new_kwds
		

def tags_to_string(tags):
	"""
	Convert a tag list to a string. 
	
	Args:
		tags (String[]): a tag list
		
	Returns:
		(String): a string where a tag is separated from its follower by ", "

	See:
		`link notewiki/doc/definitions/tag <http://notewiki.bidix.info/notewiki/doc/definitions/tag>`_.
	"""
	first = True
	s = ''
	for tag in tags:
		if not first:
			s += ', '
		else:
			first = False
		s += tag
	return s

def string_to_tags(s):
	"""
	Parse a string to a tag list. 
	
	Args:
		s (String): a string containing a tag list
		
	Returns:
		String[]: the tag list parsed from the string s

	See:
		`link notewiki/doc/definitions/tag <http://notewiki.bidix.info/notewiki/doc/definitions/tag>`_.
	"""
	if not s:
		return []
	tags = s.split(',')
	tags = [tag.strip() for tag in tags] 
	return tags

def folders(name):
	"""
	Extract folders from a name.
	
	Args:
		name (String): a  Article name
	
	Return
		[{'name':folder,'url':url}, ... ]: a list of assoc. Each assoc represents 
		a folder with its basename and its url 

	See:
		`link notewiki/doc/definitions/name <http://notewiki.bidix.info/notewiki/doc/definitions/name>`_.
	"""
	if not name:
		return None
	f = name.split('/')
	folders = []
	url=""
	first = True
	for folder in f[0:len(f)-1]:
		if first:
			first = False
		else:
			url=url+"/"
		url = url+folder
		folders.append({'name':folder,'url':url})
	return folders

def root_folder(name):
	"""
	Extract the root folder of an article name
	
	Args:
		name (String): an article name
		
	Returns:
		String: the root folder name of name
		
	See:
		`link notewiki/doc/definitions/name <http://notewiki.bidix.info/notewiki/doc/definitions/name>`_.
	"""
	if name:	
		return name.split('/')[0].strip('/')
	else:
		return None
	
def basename(name):
	"""
	Extract the basename of an article name
	
	Args:
		name (String): an article name
		
	Returns:
		String: the basename of name
		
	See:
		`link notewiki/doc/definitions/name <http://notewiki.bidix.info/notewiki/doc/definitions/name>`_.
	"""
	if name:
		f = filename(name).split('.')
		return f[0]
	else:
		return None

def dirname(name):
	"""
	Extract the dirname of an article name
	
	Args:
		name (String): an article name
		
	Returns:
		String: the dirname of name
		
	See:
		`link notewiki/doc/definitions/name <http://notewiki.bidix.info/notewiki/doc/definitions/name>`_.
	"""
	if name:
		f = name.split('/')
		return '/'.join(f[0:len(f)-1])
	else:
		return None

def filename(name):
	"""
	Extract the filename of a name or url
	
	Args:
		name (String): an article name
		
	Returns:
		String: the filename of name
		
	See:
		`link notewiki/doc/definitions/name <http://notewiki.bidix.info/notewiki/doc/definitions/name>`_.
	"""
	if name:
		f = name.split('/')
		return f[len(f)-1]
	else:
		return None

def extension(name):
	"""
	Extract extension from a name or a url
	
	Args:
		name (String): a name
		
	Returns:
		String: the extension of name
		
	See:
		`link notewiki/doc/definitions/name <http://notewiki.bidix.info/notewiki/doc/definitions/name>`_.
	"""
	f = name.split('.')
	if len(f) > 1:
		return f[len(f)-1]
	else:
		return None

def name_without_extension(name):
	"""
	Extract name without its extension from a name or a url
	
	Args:
		name (String): a name
		
	Returns:
		String: the name without extension
		
	See 
		`link notewiki/doc/definitions/name <http://notewiki.bidix.info/notewiki/doc/definitions/name>`_.
	"""
	if name:	
		return name.split('.')[0]
	else:
		return None


def parse_date(date_str):
	"""
	Parse a date exported from Datastore.
	
	Args:
		date_str(String): a Datastore date like `2011-11-04 07:49:03.527283`
	Returns:
		datetime.datetime
	"""
	date_lst = date_str.split(".")
	t = time.strptime(date_lst[0], "%Y-%m-%d %H:%M:%S")
	date= datetime.datetime(t.tm_year, t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec)
	ms= int(date_lst[1], 10)
	return date + datetime.timedelta(microseconds=ms)

def current_wikiname():
	"""
	wikiname for current user. 
	
	
	Returns:
		String
	
	See :
		function:notewiki.model.wikiname`
	"""
	current_user = users.get_current_user()
	if current_user:
		return wikiname(current_user)
	else:
		return None


def wikiname(user):
	"""
	The user's wikiname.
	
	A wikiname is user's nickname where . are replaced by _.
	
	Args:
		user(google.appengine.api.users.user): a user
	
	Returns:
		String
	
	"""
	if user:
		return user.nickname().split('@')[0].lower().replace('.','_')
	else:
		return None
		
def user_domain_role(user, name):
	"""
	Role depends on area of article.
	
	See `link notewiki/doc/definitions/role <http://notewiki.bidix.info/notewiki/doc/definitions/role>`_ 
	for Role definition upon Area.
	"""
	app = webapp2.get_app()
	area = area_for(name)
	if area == Area.shared:
		if user:
			return Role.contributor
		else:
			return Role.forbidden
	elif area == Area.private:
		if user and (root_folder(name) == '_'+wikiname(user)):
			return Role.owner
		else:
			return Role.forbidden
	else:
		return Role.reader
	
def user_app_role(user, name):
	"""
	If user don't have a profile (users/{wikiname} article)
		it can only access to public area.
	else
		he has :func:`user_domain_role` 
	"""
	area = area_for(name)
	if user:
		profile = Article.get_by_name('users/'+wikiname(user))
	else:
		profile = None
	if not profile:
		if (area == Area.public):
			return Role.reader
		else:
			return Role.forbidden
	else:
		return user_domain_role(user, name)

def user_role(name):
	"""
	The current user's :class:`notewiki.model.Role` for this article.
	
	if `app_login_control` config is set
		use :func:`user_app_role`
	else
		use :func:`user_domain_role`
		
	Args:
		name(String): name of this article
		
	Returns:
		:class:`notewiki.model.Role` 
	"""
	app = webapp2.get_app()
	if users.is_current_user_admin():
		return Role.administrator
	area = area_for(name)
	if area == Area.sandbox:
		if app.config.get('wiki_with_sandbox'):
			return Role.contributor
		else:
			return Role.reader

	user = users.get_current_user()
	if app.config.get('app_login_control'):
		return user_app_role(user, name)
	else:
		return user_domain_role(user, name)


class Entity(db.Model):
	"""
	Superclass of all entities of the model. Extends :class:`google.appengine.ext.db.Model`.
	
	Implements the import/export in XML feature of all instances of Entity. 
	The XML format of an entity is given by the to_xml() method of the :class:`google.appengine.ext.db.Model`.
	"""
	
	@classmethod
	def all_to_xml(cls):
		"""
		An XML file with all instances in XML representation.
		"""
		entity_list=cls.all().fetch(99999)
		return cls.list_to_xml(entity_list)
	
	@classmethod
	def list_to_xml(cls, entity_list):
		"""
		An XML document with all entities in list in XML representation.
		
		Args:
			entity_list(Entity[]): an Entity instance list.
		
		Returns:
			String: an XML document.
		"""
		xml='<?xml version="1.0" encoding="UTF-8"?>\n<notewiki>\n'
		for e in entity_list:
			xml = xml + e.to_xml()
		xml = xml + '</notewiki>'
		return xml

	@classmethod
	def set_from_xml(cls, xml_str):
		"""
		Add or replace Entity from the an XML document.
		
		Args:
			xml_str(String): an XML document containing Entity instances in XML format.
			
		Returns:
			Integer: number of entity inserted
		"""
		dom = xml.dom.minidom.parseString(xml_str)
		entity_list = dom.getElementsByTagName('entity')
		for entity in entity_list:
			if entity.attributes.has_key('kind'):
				cl = getattr(sys.modules[__name__], entity.attributes['kind'].nodeValue)
				obj = cl.from_xml(entity)
				obj.put()
		return len(entity_list)

class Article(Entity):
	"""
	Class for a wiki node.
	
	Articles are organized in a tree structure using their name:
	
		``[{dirname}/]{filename}``	
	
	with ``{dirname}``:	
	
		``[{root_folder}[/{folder}/.../{folder}]]``
		
	Properties:
		* name (String): required
		* title (String)
		* text (Text)
		* creator (User)
		* editor (User)
		* created (DateTime)
		* modified (DateTime)
		* tags (String[]): computed from the tag(String) keyword
		* folder (String): extracted from name

	See:
		`link notewiki/doc/definitions/article <http://notewiki.bidix.info/notewiki/doc/definitions/article>`_.
	
	"""
	#key_name always present : key().name()
	name = db.StringProperty(required=True)
	title = db.StringProperty()
	text = db.TextProperty()
	creator = db.UserProperty(auto_current_user_add=True)
	editor = db.UserProperty(auto_current_user=True)
	created = db.DateTimeProperty(required=True, auto_now_add=True)
	modified = db.DateTimeProperty(required=True, auto_now=True)
	tags = db.StringListProperty()
	folder = db.StringProperty()
	
	def __init__(self,
			   parent=None,
			   key_name=None,
			   _app=None,
			   _from_entity=False,
			   **kwds):
		"""
		name is Mandatory
		key_name contains the same value as name
		folder is set from name
		tags is computed from tag
		"""
		if key_name and not kwds.has_key('name'):
			kwds['name'] = key_name
		if kwds.has_key('name') and kwds['name']:
			key_name = kwds['name']
			pos = kwds['name'].rfind('/')
			if pos > -1:
				kwds['folder'] = kwds['name'][0:pos]
		if kwds.has_key('tag'):
			kwds['tags'] = string_to_tags(kwds['tag'])
		db.Model.__init__(self, key_name=key_name, **kwds)

#	@deprecated
#	def add_child(self, child):
#		"""
#		
#		"""
#		self.children.append(child)
#		self.put()

	@classmethod
	def add_or_replace(cls, **kwds):
		"""
		Create or replace article from keywords.
		
		If article exists, preserve created property. The tags property is computed from tag keyword.
		
		Args:
			kwds: see :class:`notewiki.model.Article` documentation
			
		Returns:
			Article
		"""
		e = cls.get_by_name(kwds['name'])
		if kwds.has_key('tag'):
			kwds['tags'] = string_to_tags(kwds['tag'])
		if not e:
#			e = cls(**convert_unicode_keywords(kwds))			
			e = cls(**kwds)			
		else:
			for (key, value) in kwds.iteritems():
				if cls.properties().has_key(key) and (key != 'name') and (key != 'key_name'):
					setattr(e, key, value)
		e.put()
		return e
	
	@classmethod
	def _create_empty_article(cls, name):
		"""
		Create an empty article if it does not exists.
		
		Used recursively when an article is created to create its folder.
		
		Args:
			name(String): name of the article to create.
		"""
		if name:
			folder = Article.get_by_key_name(name)
			if not folder:
				folder = Article(key_name=name)
				folder.put()

	@classmethod
	def from_xml(cls, xml_node):
		"""
		Create or update an article from an XML node.
		
		Args:
			xml_node(String)
			
		Returns:
			Article
		"""
		properties = xml_node.getElementsByTagName('property')
		members={}
		members['tags'] =[]
		for property in properties:
			p_name = property.attributes['name'].nodeValue
			if len(property.childNodes) > 0:	
				value = property.childNodes[0].data
				if p_name in ['creator', 'editor']:
					members[p_name] = users.User(value)
				elif p_name in ['created', 'modified']:
					members[p_name] = parse_date(value)
				elif p_name == 'tags':
					members[p_name].append(value)
				else:
					members[p_name] = value
			else:
				if p_name == 'tags':
					members[p_name] = []
				else:	
					members[p_name] = None
		logging.debug("about to add_or_update Article %s"%members['name'])
		# return cls.add_or_replace(**convert_unicode_keywords(members))
		return cls.add_or_replace(**members)

	@classmethod
	def get_by_name(cls, name):
		"""
		Get instance of Article class by its key's name.
		
		Args:
			name(String): name of article
		
		Returns:
			Article
		"""
		return cls.get_by_key_name(name)

	def get_parent(self):	
		"""
		Get parent of this Article.
		
		Returns:
			Article
		"""
		if self.folder:
			return Article.get_by_name(self.folder)

	@classmethod
	def list_all(cls, folder=None):
		"""
		Get all sub-articles of folder. If folder is None, list all articles. 
		
		Args:
			folder(String): name of parent folder.
		
		Returns:
			Article[]
		"""
		if folder and (folder != ' ') :
			query = db.GqlQuery("SELECT * FROM Article " +
							"WHERE folder = :1 " +
							" ORDER BY name",
							folder)
		else:
			query = db.GqlQuery("SELECT * FROM Article " +
							" ORDER BY name")
			
		r = query.fetch(9999)
		return r;

	@classmethod
	def list_all_visible(cls, user, folder=None):	
		"""
		Get all sub-articles of folder visible by user. If folder is None, list all articles. 
		
		Args:
			user(google.appengine.api.users.user): user.
			folder(String): name of parent folder.
		
		Returns:
			Article[]
		"""
		article_list = cls.list_all(folder)
		visible_list = []
		for a in article_list:
			role = user_role(a.name)
			if role >= Role.reader:
				visible_list.append(a)
		return visible_list
	
	@classmethod
	def list_with_tag(cls, tag):
		"""
		Get all articles with tag *tag*. 
		
		Args:
			tag(String): tag to filter articles.
		
		Returns:
			Article[]
		"""
		query = db.GqlQuery("SELECT * FROM Article " +
							"WHERE tags = :1 " +
							" ORDER BY name",
							tag)
		r = query.fetch(9999)
		return r;
	
	@classmethod
	def list_with_regex(cls, user, exp):
		"""
		Get all articles, visible by *user*, that match *exp*.
		
		*exp* is compiled using the *IGNORECASE* flag.
		
		Args:
			user(google.appengine.api.users.user): user.
			exp(String): regular expression to filter articles.
		
		Returns:
			Article[]
		"""
		articles = cls.list_all_visible(user)
		match_list = []
		regex = re.compile(exp, re.IGNORECASE)
		for article in articles:
			if (article.title and regex.search(article.title)) or (article.text and regex.search(article.text)):
				match_list.append(article)
		return match_list
			
	def put(self):
		"""
		Hook after google.appengine.ext.db.Model.put to create recursively empty parent articles.
		"""
		k = db.Model.put(self)
		self._create_empty_article(self.folder)
		return k

	def rename(self, new_name):
		"""
		Create a new article with *new_name* and delete self.
		
		Args:
			new_name(String)
			
		Returns:
			Article: new article
		"""
		new_article = Article(key_name=new_name, 
							name=new_name, title=self.title, 
							text=self.text, 
							tags=self.tags, 
							created=self.created, 
							creator=self.creator)
		new_article.put()
		self.delete()
		return new_article
	
	@classmethod
	def title_for_name(cls, name):
		"""
		Return title for article with name *name*.
		
		Args:
			name(String): article name
			
		Retunns
			String: title of article
		"""
		article = cls.get_by_name(name)
		if article:
			return article.title
		else:
			return None
		
	def to_context(self):
		"""
		Returns a context for article suitable to django template.
		
		The context is a dictionary with a key by article data member:
			* name
			* title
			* text
			* creator
			* editor
			* cretaed
			* modified
			* tags
			* folders
			* filename
			* basename
			* dirname
		
		"""
		if self.title:
			title = self.title
		else:
			if self.name:
				title = ''
			else:
				title = '/'
		context =  { 
			'name': render_str(self.name),
			'title': render_str(title),
			'text': render_str(self.text),
			'creator': render_user(self.creator),
			'editor': render_user(self.editor),
			'created': self.created,
			'modified': self.modified,
			'tags': self.tags,
			'folders': folders(self.name),
			'filename': filename(self.name),
			'basename': basename(self.name),
			'dirname': dirname(self.name)
			}
		return context

	@classmethod
	def get_all_tags(cls, user):
		"""
		Returns, computed with articles visible by user, a dictionary with tag as key and number of articles with corresponding tag as value. 
		"""
		articles = cls.list_all_visible(user)
		tags = {}
		for article in articles:
			ts = article.tags
			for tag in ts:
				if not tags.has_key(tag):
					tags[tag] = 1
				else:
					tags[tag] += 1
		return tags


class Profile(Entity):
	"""
	TODO: document this class
	
	Work in progress...
	
	Manage a user profile in an Entity
	"""
	name = db.StringProperty(required=True)
	guser = db.UserProperty(required=True)
	real_name = db.StringProperty()
	title = db.StringProperty()
	bio = db.TextProperty()
	web = db.StringProperty()
	photo_url = db.StringProperty()
	tos = db.BooleanProperty() # term of service validated

	@classmethod
	def check_access_for_user(cls, user):
		"Get profile for user and check access"
		profile = Profile.get_for_user(user)
		# access if profile exists
		return profile

	
	@classmethod
	def get_for_name(cls, name):
		"Get profile for name"
		query = db.GqlQuery("SELECT * FROM Profile " +
							"WHERE name = :1 ",
							name)
		
		profiles = query.fetch(1)
		if not profiles:
			return None
		profile = profiles[0]
		return profile
			
	@classmethod
	def get_for_user(cls, user):
		"Get profile for user"
		query = db.GqlQuery("SELECT * FROM Profile " +
							"WHERE guser = :1 ",
							user)
		
		profiles = query.fetch(1)
		if not profiles:
			return None
		profile = profiles[0]
		return profile



class Role:
	"""
	Role of the current user in the context of the current article.
	
	The role of the user can be:
	
	* *reader*: can only read
	* *contributor*: can read or edit
	* *owner*: user is the owner of this article. The article is located in his provate area
	* *administrator*: all rights on this article.
	* *forbidden*: this article does not exist for the user. The arae is not accesible.
	
	see `link notewiki/doc/definitions/role <http://notewiki.bidix.info/notewiki/doc/definitions/role>`_.
	 
	"""
	forbidden = 0
	reader = 1
	contributor = 2
	owner = 3
	administrator = 4	
		
class Area:
	"""
	An Area is characterized by its accessibility.
		
	An Area can be:
	
	* *public*: every user can read
	* *shared*: logged user can read and edit
	* *private*: only logged user (and admin) can read and edit 
	* *sandbox*: every user vcan read and edit
	* *unknown*: undermined.
	
	see `link notewiki/doc/definitions/area <http://notewiki.bidix.info/notewiki/doc/definitions/area>`_.
	
	"""
	unknown = 0
	public = 1
	shared = 2
	private = 3
	sandbox = 4
